﻿// Deep Earth is a community project available under the Microsoft Public License (Ms-PL)
// Code is provided as is and with no warranty – Use at your own risk
// View the project and the latest code at http://DeepEarth.codeplex.com/

using System;
using System.Windows;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Shapes;
using DeepEarth.Client.MapControl.Events;
using MouseWheelEventArgs = DeepEarth.Client.MapControl.Events.MapMouseWheelEventArgs;

namespace DeepEarth.Client.MapControl.Controls
{
    /// <summary>
    /// <para>
    /// UI control to handle the Pan and Select behaviours of the mouse on the map
    /// </para>
    /// <example>
    /// 
    /// <code lang="XAML" title="Add MouseControl via Xaml, is located in the map style/template">   
    /// <![CDATA[
    /// <Controls:MouseControl x:Name="PART_MouseControl"/>
    /// ]]> 
    /// </code>
    /// </example>
    /// </summary>
    public class MouseControl : MapControl
    {
        private Rectangle _box;
        private DragBehavior _dragModePrior;
        private bool _isCtrlDown;
        private bool _moved;
        private Point _origMousePoint;
        private bool _isMouseDown;
        private bool _isMouseOver;
        private Point _mouseLogicalPosition;
        private Point _mousePixelPosition;
        private Point _originOnClick;

        protected bool IsWheelEnabled { get; set; }

        /// <summary>
        /// MouseControl Constructor, will use the default instance of the map
        /// </summary>
        public MouseControl() : this(Map.DefaultInstance) { }

        /// <summary>
        /// MouseControl Constructor, will use the default instance of the map
        /// </summary>
        /// <param name="map">Instance of Map</param>
        public MouseControl(Map map)
        {
            DefaultStyleKey = typeof(MouseControl);


            if (!DesignerProperties.IsInDesignTool)
            {
                _MapInstance = map;
                IsWheelEnabled = true;
            }
        }

        /// <summary>
        /// Gets zoom level adjustment value, default is 1
        /// </summary>
        public int ZoomLevelAdjustment
        {
            get { return 1; }
        }

        /// <summary>
        /// Gets mouse wheel zoom level adjustment value, default is 1
        /// </summary>
        public int WheelZoomLevelAdjustment
        {
            get { return 1; }
        }

        /// <summary>
        /// Gets MouseControl Bounds, Rect with Height and Width of the area covered by Mouse Drag when in selection DragBehavour mode
        /// </summary>
        public Rect Bounds
        {
            get
            {
                var bounds = new Rect(Location, new Size(_box.Width, _box.Height));
                return bounds;
            }
        }

        /// <summary>
        /// Gets MouseControl Size, Height and Width of the area covered by Mouse Drag when in selection DragBehavour mode
        /// </summary>
        public Size Size
        {
            get { return new Size(_box.Width, _box.Height); }
            set
            {
                _box.Width = value.Width;
                _box.Height = value.Height;

                Visibility = value.IsEmpty == false ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        /// <summary>
        /// Gets MouseControl Location, Point X,Y for Top Left relative to the Canvas containing the MouseControl
        /// </summary>
        public Point Location
        {
            get
            {
                return new Point(Canvas.GetLeft(_box), Canvas.GetTop(_box));
            }
            set
            {
                Canvas.SetTop(_box, value.Y);
                Canvas.SetLeft(_box, value.X);
            }
        }

        /// <summary>
        /// Overrides the default OnApplyTemplate to configure child FrameworkElement references from the applied template
        /// </summary>
        public override void OnApplyTemplate()
        {
            _box = (Rectangle)GetTemplateChild("PART_PixelBox");

            // Test IsDesignTime, to help display control in blend correctly
            if (!DesignerProperties.IsInDesignTool)
            {
                //Wire up the mouse events to fire the Map.Events.Map*Events.
                MapInstance.MouseEnter += MapInstance_MouseEnter;
                MapInstance.MouseLeave += MapInstance_MouseLeave;

                //MapInstance.MouseEnter += (o, e) => MapInstance.Events.MouseEnter(MapInstance, e);
                //MapInstance.MouseLeave += (o, e) => MapInstance.Events.MouseLeave(MapInstance, e);

                MapInstance.MouseLeftButtonDown += (o, e) => { IsWheelEnabled = false; MapInstance.Events.MouseDown(MapInstance, e); };
                MapInstance.MouseLeftButtonUp += (o, e) => { IsWheelEnabled = true; MapInstance.Events.MouseUp(MapInstance, e); };
                MapInstance.MouseMove += (o, e) => MapInstance.Events.MouseMove(MapInstance, e);

                //Create event handlers for standard MouseControl behaviour.
                MapInstance.Events.MapMouseDown += Events_MapMouseDown;
                MapInstance.Events.MapMouseMove += Events_MapMouseMove;
                MapInstance.Events.MapMouseDrag += Events_MapMouseDrag;
                MapInstance.Events.MapMouseUp += Events_MapMouseUp;
                MapInstance.Events.MapKeyDown += Events_MapKeyDown;
                MapInstance.Events.MapKeyUp += Events_MapKeyUp;
                MapInstance.Events.MapMouseEnter += Events_MouseEnter;
                MapInstance.Events.MapMouseLeave += Events_MouseLeave;
                MapInstance.Events.MapMouseWheel += Events_MapMouseWheel;
                MapInstance.Events.MapDragBehavourChanged += Events_MapDragBehavourChanged;

                // this event handler just passes/fires the SL 4 mouse wheel event to the original DE mouse wheel events 
                MapInstance.MouseWheel += MapInstance_MouseWheel;
            }
            else
            {
                // Is DesignTime, so set some dummy Height & Widths for editing control in Blend
                Width = 200;
                Height = 200;
                _box.Width = 200;
                _box.Height = 200;
            }
        }

        // this event handler just passes/fires the SL 4 mouse wheel event to the original DE mouse wheel events 
        private void MapInstance_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            Events_MapMouseWheel(sender, new MouseWheelEventArgs(e.Delta));
        }

        private static void Events_MapDragBehavourChanged(object sender, MapEventArgs args)
        {
            var map = (Map)sender;

            if (map.DragMode == DragBehavior.Pan || map.DragMode == DragBehavior.Select)
            {
                map.Events.EnableMouseWheel = true;
                map.Events.EnableKeyboard = true;
                map.Events.EnableMapZoom = true;
                map.Events.EnableMouseClicks = true;

                //map.Events.Enable = true;
            }
        }

        Control _focusElement;
        void MapInstance_MouseEnter(object sender, MouseEventArgs e)
        {
            var focusObject = FocusManager.GetFocusedElement();
            if (focusObject != this)
            {
                if (focusObject != null && focusObject is Control)
                {
                    _focusElement = focusObject as Control;
                }
                else
                {
                    _focusElement = null;
                }
            }
            else
            {
                _focusElement = null;
            }
            MapInstance.Focus();
            MapInstance.Events.MouseEnter(MapInstance, e);
        }


        void MapInstance_MouseLeave(object sender, MouseEventArgs e)
        {
            if (_focusElement != null)
            {
                _focusElement.Focus();
            }
            MapInstance.Events.MouseLeave(MapInstance, e);
        }



        private void Events_MapMouseWheel(object sender, MouseWheelEventArgs args)
        {
            var map = (Map)sender;

            if (!_isMouseOver || args.Delta == 0)
            {
                return;
            }

            var adjustment = args.Delta > 0 ? WheelZoomLevelAdjustment : -WheelZoomLevelAdjustment;
            map.ZoomOnPixelPoint(_mousePixelPosition, adjustment);
            args.Handled = true;
        }



        private void Events_MapKeyDown(object sender, KeyEventArgs args)
        {
            var map = (Map)sender;

            if (args.Key == Key.Ctrl && _isCtrlDown == false)
            {
                _dragModePrior = MapInstance.DragMode;
                _isCtrlDown = true;
                map.DragMode = DragBehavior.Select;
            }
            else
            {
                switch (args.Key)
                {
                    //For convenience, hold Control to zoom when already navigating with the arrows
                    case Key.Left: map.Pan(Direction.West); break;
                    case Key.Right: map.Pan(Direction.East); break;
                    case Key.Up: if (_isCtrlDown) map.ZoomLevel += 1; else map.Pan(Direction.North); break;
                    case Key.Down: if (_isCtrlDown) map.ZoomLevel -= 1; else map.Pan(Direction.South); break;
                }
            }
        }

        private void Events_MapKeyUp(object sender, KeyEventArgs args)
        {
            if (args.Key == Key.Ctrl)
            {
                var map = (Map)sender;
                _isCtrlDown = false;
                map.DragMode = _dragModePrior;
            }
        }

        private void Events_MapMouseDown(object sender, MouseButtonEventArgs args)
        {
            CaptureMouse();
            _isMouseDown = true;

            var map = (Map)sender;

            _origMousePoint = args.GetPosition(map);

            switch (MapInstance.DragMode)
            {
                case DragBehavior.Select:
                    Size = new Size();
                    Location = _origMousePoint;
                    break;

                case DragBehavior.Pan:
                    _originOnClick = map.LogicalOrigin;
                    break;
            }
        }


        private void Events_MapMouseMove(object sender, MouseEventArgs args)
        {
            var map = (Map)sender;

            if (_origMousePoint == new Point())
            {
                _origMousePoint = args.GetPosition(map);
            }

            _mousePixelPosition = args.GetPosition(map);
            _mouseLogicalPosition = map.CoordHelper.PixelToLogicalIncRotation(_mousePixelPosition);
        }


        private void Events_MapMouseDrag(object sender, MouseEventArgs args)
        {
            var map = (Map)sender;

            switch (MapInstance.DragMode)
            {
                case DragBehavior.Pan:
                    {
                        if (_isMouseDown)
                        {
                            //Check to see that we are not exceeding the Map Boundaries.
                            var geoPoint = _MapInstance.CoordHelper.LogicalToGeo(_MapInstance.LogicalCenter);
                            var inBounds = MapInstance.BaseLayer.Source.IsValidGeoPoint(geoPoint);

                            if (inBounds)
                            {
                                var newPoint = _originOnClick;
                                var origMousePointLogical = map.CoordHelper.PixelToLogicalIncRotation(_origMousePoint);
                                newPoint.X += (origMousePointLogical.X - _mouseLogicalPosition.X);
                                newPoint.Y += (origMousePointLogical.Y - _mouseLogicalPosition.Y);
                                map.LogicalOrigin = newPoint;
                            }
                        }
                        break;
                    }

                case DragBehavior.Select:
                    {
                        if (_isMouseDown)
                        {
                            _moved = true;
                            Point loc = args.GetPosition(map);
                            Point boundedLoc = GetBoundedPixel(map, ref loc);

                            Location = new Point(Math.Min(_origMousePoint.X, boundedLoc.X), Math.Min(_origMousePoint.Y, boundedLoc.Y));
                            var newSize = new Size(Math.Abs(_origMousePoint.X - boundedLoc.X), Math.Abs(_origMousePoint.Y - boundedLoc.Y));
                            Size = newSize;
                        }
                        break;
                    }

            }

        }

        private static Point GetBoundedPixel(object sender, ref Point loc)
        {
            var map = (Map)sender;

            //Incorp Provider Source boundaries to not go outside of Map bounds
            var bounds = map.BaseLayer.Source.CoordinateBounds;
            var mouseGeoPoint = map.CoordHelper.PixelToGeo(loc);
            mouseGeoPoint.Longitude = Math.Min(mouseGeoPoint.Longitude, bounds.Right);
            mouseGeoPoint.Longitude = Math.Max(mouseGeoPoint.Longitude, bounds.Left);
            mouseGeoPoint.Latitude = Math.Max(mouseGeoPoint.Latitude, bounds.Top);
            mouseGeoPoint.Latitude = Math.Min(mouseGeoPoint.Latitude, bounds.Bottom);
            var boundedLoc = map.CoordHelper.GeoToPixel(mouseGeoPoint);

            return boundedLoc;
        }

        private void Events_MapMouseUp(object sender, MouseButtonEventArgs args)
        {
            var map = (Map)sender;
            ReleaseMouseCapture();

            switch (MapInstance.DragMode)
            {
                case DragBehavior.Select:
                    if (_moved) MapInstance.PixelBounds = Bounds;
                    break;

                case DragBehavior.Pan:
                    (map).ReleaseMouseCapture();
                    break;
            }

            _isMouseDown = false;
            _moved = false;
            _isMouseDown = false;
            Size = new Size();

        }

        private void Events_MouseEnter(object sender, MouseEventArgs args)
        {
            _isMouseOver = true;
        }

        private void Events_MouseLeave(object sender, MouseEventArgs args)
        {
            _isMouseOver = false;
        }
    }
}







//#region MouseWheelListener APIs



///// <summary>
///// Indicates whether or not the mouse wheel is enabled.
///// </summary>
//public bool IsWheelEnabled { get; set; }


///// <summary>
///// Handles mouse wheel events for Firefox.
///// </summary>
///// <param name="sender">The HTML element for the plug-in.</param>
///// <param name="e">The HTML event arguments.</param>
//private void OnMouseWheelFirefox(object sender, HtmlEventArgs e)
//{
//    if(_IsMouseOver == false)
//    {
//        return;
//    }

//    if(IsWheelEnabled == false)
//    {
//        e.PreventDefault();
//        return;
//    }

//    double delta = (double)e.EventObject.GetProperty("detail") / -3;
//    var args = new MouseWheelEventArgs(delta);
//    MapInstance.Events.MouseWheel(MapInstance, args);

//    if(args.Handled)
//    {
//        e.PreventDefault();
//    }
//}

///// <summary>
///// Handles mouse wheel events for browsers other than Firefox.
///// </summary>
///// <param name="sender">The HTML element for the plug-in.</param>
///// <param name="e">The HTML event arguments.</param>
//private void OnMouseWheelOther(object sender, HtmlEventArgs e)
//{
//    if(_IsMouseOver == false)
//    {
//        return;
//    }

//    if(IsWheelEnabled == false)
//    {
//        e.EventObject.SetProperty("returnValue", false);
//        return;
//    }

//    double delta = (double)e.EventObject.GetProperty("wheelDelta") / 120;
//    var args = new MouseWheelEventArgs(delta);
//    MapInstance.Events.MouseWheel(MapInstance, args);

//    if(args.Handled)
//    {
//        e.EventObject.SetProperty("returnValue", false);
//    }
//}



//#endregion